iT邦幫忙

2021 iThome 鐵人賽

DAY 5
1
Mobile Development

Andoroid - Kotlin筆記 (新)系列 第 5

[Day5] Android - Kotlin筆記:ListAdapter + DiffUtil 進階應用 - 複數itemViewType

  • 分享至 

  • xImage
  •  

Problem

昨天我們提到ListAdapter + DiffUtil在一般RecyclerView的基本使用。
而實際上工作中我們經常會需要在RecyclerView上顯示不同的itemView
我們會添加FooterHeader,或是不同樣式的itemView
這時候該怎麼辦呢?


Solution

submitList之前,把dataList中的item順序重新編排後,
再使用submitList提交我們的dataList做更新。


實作

假設我們要做一個像Line一樣的聊天室,
而且底部要添加一個名為“已滑至底”的TextView做為Footer

資料的部分會用Message這個data class來顯示訊息,
並根據isFromMe來判斷是否為自己發送的訊息,展示不同的`itemView

data class Message(
    val id: Long,
    val timeStamp: Long,
    val isFromMe: Boolean,
    val message: String
)

1. ListAdapter

首先,因為會展示不同的項目,我們不餵ListAdapterdata class了,
改吃自己創建的sealed class - 這邊我們命名為DataItem

class MessageAdapter() : ListAdapter<DataItem, RecyclerView.ViewHolder>(DiffCallback()) {}

2. 類別控管

我們添加一個sealed class
這個sealed class負責用來控管不同item的型態類別。
這邊我們列舉出ItemFooter這兩個型態(data type)。

因為DiffUtil需要一個參數作為判斷新舊item是否一樣的依據。
所以我們創建一個abstract item id作為interface回傳判別用的數據。

isFromMe則是用來判斷顯示訊息在左側還是右側的item view

sealed class DataItem {

    abstract val id: Long
    abstract val isFromMe: Boolean

    data class Item(val message: Message) : DataItem() {
        override val id = message.id
        override val isFromMe = message.isFromMe
    }

    object Footer : DataItem() {
        override val id = Long.MIN_VALUE
        override val isFromMe = false
    }
}

(關於sealed class後續文章有機會會講解,或是你也可以看這篇寫得很詳盡)

這邊因為Footer只是作為靜態顯示layout
因此只使用object而非data class


3. DiffUtil

DiffUtil也改為判斷DataItem中的id

    class DiffCallback : DiffUtil.ItemCallback<DataItem>() {
        override fun areItemsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
            return oldItem == newItem
        }

    }

4. ItemViewType

創建一個enum class列舉需展示的所有viewType

    enum class ItemViewType {
        MESSAGE_FROM_ME, MESSAGE_TO_ME, FOOTER
    }

這邊列舉viewType,是給Adapter判斷要展示哪個ViewHolder來使用的。


5. getItemViewType

因為我們有不同的型別需判斷,
所以我們要覆寫getItemViewType
鍵盤按下control+o,選擇getItemViewType並覆寫他。

    override fun getItemViewType(position: Int): Int {
        return when (getItem(position)) {
            is DataItem.FromMe -> ItemViewType.MESSAGE_FROM_ME.ordinal
            is DataItem.ToMe -> ItemViewType.MESSAGE_TO_ME.ordinal
            is DataItem.Footer -> ItemViewType.FOOTER.ordinal
        }
    }

這邊返回的int是onCreateViewHolder會用到的viewType,繼續往下看下去。


6. 新增onCreateViewHolderonBindViewHolder判斷

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            ItemViewType.MESSAGE_FROM_ME.ordinal -> FromMeViewHolder.from(parent)
            ItemViewType.MESSAGE_TO_ME.ordinal -> ToMeViewHolder.from(parent)
            else -> FooterViewHolder.from(parent)
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {

            is FromMeViewHolder -> {
                val data = getItem(position) as DataItem.Item
                holder.bind(data.message)
            }

            is ToMeViewHolder -> {
                val data = getItem(position) as DataItem.Item
                holder.bind(data.message)
            }

            is FooterViewHolder -> {
            }
        }
    }

7. 新增對應的ViewHolder

FromMeToMeFooter創建對應的ViewHolder

    class FromMeViewHolder private constructor(val binding: ItemAccountHistoryNextContentBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(message: Message) {
            itemView.apply {
                tv_message_from_me.text = message.content
            }
        }

        companion object {
            fun from(parent: ViewGroup): RecyclerView.ViewHolder {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.itemview_message_from_me, parent, false)
                return ToMeViewHolder(view)
            }
        }


    }

    class ToMeViewHolder (view: View) : RecyclerView.ViewHolder(view) {

        fun bind(message: Message) {
            itemView.apply {
                tv_message_to_me.text = message.content
            }
        }

        companion object {
            fun from(parent: ViewGroup): RecyclerView.ViewHolder {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.itemview_message_to_me, parent, false)
                return ToMeViewHolder(view)
            }
        }

    }

    class FooterViewHolder (view: View) : RecyclerView.ViewHolder(view) {
        companion object {
            fun from(parent: ViewGroup): RecyclerView.ViewHolder {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.itemview_footer, parent, false)
                return FooterViewHolder(view)
            }
        }
    }

8. submitList

最後一步了!
現在因為ListAdapter吃的是DataItem,
我們只要創建一個function - addFooterAndSubmitList來取代原本的submitList
利用DataItem整理過後再交給submitList處理就大功告成。

    private val adapterScope = CoroutineScope(Dispatchers.Default)

    fun addFooterAndSubmitList(list: List<Message>) {
        adapterScope.launch {
            val items = list.map { 
                if (it.isFromMe) DataItem.FromMe(it)
                else DataItem.ToMe(it)
            } + listOf(DataItem.Footer)
            withContext(Dispatchers.Main) { //update in main ui thread
                submitList(items)
            }
        }
    }

只要透過呼叫addFooterAndSubmitList就能讓ListAdapter成功運作,
DiffUtil自動去篩選判斷更新的內容。

    rvAdapter.addFooterAndSubmitList(dataList)

全程式碼展示:

  • Adapter

data class Message(
    val id: Long,
    val timeStamp: Long,
    val isFromMe: Boolean,
    val content: String
)

class MessageAdapter() : ListAdapter<DataItem, RecyclerView.ViewHolder>(DiffCallback()) {

    enum class ItemViewType {
        MESSAGE_FROM_ME, MESSAGE_TO_ME, FOOTER
    }

    private val adapterScope = CoroutineScope(Dispatchers.Default)

    fun addFooterAndSubmitList(list: List<Message>) {
        adapterScope.launch {
            val items = list.map {
                if (it.isFromMe) DataItem.FromMe(it)
                else DataItem.ToMe(it)
            } + listOf(DataItem.Footer)
            withContext(Dispatchers.Main) { //update in main ui thread
                submitList(items)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            ItemViewType.MESSAGE_FROM_ME.ordinal -> FromMeViewHolder.from(parent)
            ItemViewType.MESSAGE_TO_ME.ordinal -> ToMeViewHolder.from(parent)
            else -> FooterViewHolder.from(parent)
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {

            is FromMeViewHolder -> {
                val data = getItem(position) as DataItem.FromMe
                holder.bind(data.message)
            }

            is ToMeViewHolder -> {
                val data = getItem(position) as DataItem.ToMe
                holder.bind(data.message)
            }

            is FooterViewHolder -> {
            }
        }
    }

    override fun getItemViewType(position: Int): Int {
        return when (getItem(position)) {
            is DataItem.FromMe -> ItemViewType.MESSAGE_FROM_ME.ordinal
            is DataItem.ToMe -> ItemViewType.MESSAGE_TO_ME.ordinal
            is DataItem.Footer -> ItemViewType.FOOTER.ordinal
        }
    }


    class FromMeViewHolder private constructor(val binding: ItemAccountHistoryNextContentBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(message: Message) {
            itemView.apply {
                tv_message_from_me.text = message.content
            }
        }

        companion object {
            fun from(parent: ViewGroup): RecyclerView.ViewHolder {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.itemview_message_from_me, parent, false)
                return ToMeViewHolder(view)
            }
        }


    }

    class ToMeViewHolder (view: View) : RecyclerView.ViewHolder(view) {

        fun bind(message: Message) {
            itemView.apply {
                tv_message_to_me.text = message.content
            }
        }

        companion object {
            fun from(parent: ViewGroup): RecyclerView.ViewHolder {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.itemview_message_to_me, parent, false)
                return ToMeViewHolder(view)
            }
        }

    }

    class FooterViewHolder (view: View) : RecyclerView.ViewHolder(view) {
        companion object {
            fun from(parent: ViewGroup): RecyclerView.ViewHolder {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.itemview_footer, parent, false)
                return FooterViewHolder(view)
            }
        }
    }

    class DiffCallback : DiffUtil.ItemCallback<DataItem>() {
        override fun areItemsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
            return oldItem == newItem
        }

    }
}

sealed class DataItem {

    abstract val id: Long
    abstract val isFromMe: Boolean

    data class FromMe(val message: Message) : DataItem() {
        override val id = message.id
        override val isFromMe = message.isFromMe
    }

    data class ToMe(val message: Message) : DataItem() {
        override val id = message.id
        override val isFromMe = message.isFromMe
    }

    object Footer : DataItem() {
        override val id = Long.MIN_VALUE
        override val isFromMe = false
    }

}

上一篇
[Day4] Android - Kotlin筆記:RecyclerView Adapter - ListAdapter + DiffUtil
下一篇
[Day6] Android - Kotlin筆記:RecycledViewPool
系列文
Andoroid - Kotlin筆記 (新)18
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言